﻿using FastEmit;
using System.Globalization;
using System.Text;
using System.Text.Json;

namespace InfluxDB.Flurl
{
    public class InfluxDbWriter
    {
        #region static

        public static readonly byte[] N = [(byte)'\n'];

        private static void EscapeKey(StringBuilder sb, string key, bool escapeEqual = true)
        {
            foreach (var c in key)
            {
                switch (c)
                {
                    case '\n':
                        sb.Append("\\n");
                        continue;
                    case '\r':
                        sb.Append("\\r");
                        continue;
                    case '\t':
                        sb.Append("\\t");
                        continue;
                    case ' ':
                    case ',':
                        sb.Append('\\');
                        break;
                    case '=':
                        if (escapeEqual)
                        {
                            sb.Append('\\');
                        }

                        break;
                }

                sb.Append(c);
            }
        }

        private static void EscapeValue(StringBuilder sb, string value)
        {
            foreach (var c in value)
            {
                switch (c)
                {
                    case '\\':
                    case '\"':
                        sb.Append('\\');
                        break;
                }

                sb.Append(c);
            }
        }

        private static bool IsNotDefined(object value)
        {
            return value == null
                   || value is double d && (double.IsInfinity(d) || double.IsNaN(d))
                   || value is float f && (float.IsInfinity(f) || float.IsNaN(f));
        }

        private static void SetFields(StringBuilder sb, string key, object val)
        {
            if (IsNotDefined(val))
            {
                throw new Exception($"[Field] {key} value is null");
            }

            EscapeKey(sb, key);
            sb.Append('=');

            if (val is double || val is float)
            {
                sb.Append(((IConvertible)val).ToString(CultureInfo.InvariantCulture));
            }
            else if (val is uint || val is ulong || val is ushort)
            {
                sb.Append(((IConvertible)val).ToString(CultureInfo.InvariantCulture));
                sb.Append('u');
            }
            else if (val is byte || val is int || val is long || val is sbyte || val is short)
            {
                sb.Append(((IConvertible)val).ToString(CultureInfo.InvariantCulture));
                sb.Append('i');
            }
            else if (val is bool b)
            {
                sb.Append(b ? "true" : "false");
            }
            else if (val is string s)
            {
                sb.Append('"');
                EscapeValue(sb, s);
                sb.Append('"');
            }
            else if (val is JsonElement el)
            {
                if (el.ValueKind == JsonValueKind.String)
                {
                    sb.Append('"');
                    EscapeValue(sb, el.GetString());
                    sb.Append('"');
                }
                else if (el.ValueKind == JsonValueKind.True)
                {
                    sb.Append("true");
                }
                else if (el.ValueKind == JsonValueKind.False)
                {
                    sb.Append("false");
                }
                else if (el.ValueKind == JsonValueKind.Number)
                {
                    var rowTxt = el.GetRawText();
                    var isDecimal = rowTxt.Contains('.');
                    sb.Append(rowTxt);
                    if (!isDecimal)
                    {
                        sb.Append('i');
                    }
                }
                else
                {
                    throw new Exception($"{key} the [JsonElement] value=>{val} not support.");
                }
            }
            else if (val is IConvertible c)
            {
                sb.Append(c.ToString(CultureInfo.InvariantCulture));
            }
            else
            {
                sb.Append('"');
                EscapeValue(sb, val.ToString());
                sb.Append('"');
            }
        }

        private static string CreateTypeData<T>(string measurement, T data, TypeAccessor accessor, TypeCache ch)
        {
            var sb = new StringBuilder();

            sb.Append(measurement);
            sb.Append(',');

            //append tags
            var i = 0;
            foreach (var key in ch.TagKeys)
            {
                var val = accessor.GetValue(data, key)
                    ?? throw new Exception($"[Tag] {key} the value is null.");

                i++;
                if (i > 1)
                {
                    sb.Append(',');
                }

                sb.Append(key);
                sb.Append('=');
                string valStr = (string)val;
                if (valStr == "")
                {
                    throw new Exception($"[Tag] {key} the value is empty.");
                }
                EscapeKey(sb, valStr);
            }

            //append fileds
            sb.Append(' ');
            i = 0;
            foreach (var key in ch.FieldKeys)
            {
                var val = accessor.GetValue(data, key)
                    ?? throw new Exception($"[Field] {key} the value is null.");

                i++;
                if (i > 1)
                {
                    sb.Append(',');
                }

                SetFields(sb, key, val);
            }

            //append time
            sb.Append(' ');
            var valTime = accessor.GetValue(data, ch.TimeKey);
            if (valTime != null)
            {
                sb.Append(TimeUtils.DateTimeOffsetToUtcString((DateTimeOffset)valTime, ch.Precision));
            }
            else
            {
                sb.Append(TimeUtils.DateTimeOffsetToUtcString(DateTimeOffset.UtcNow, ch.Precision));
            }

            return sb.ToString();
        }

        #endregion

        private readonly Stream _stream;
        private readonly CancellationToken _token;

        public InfluxDbWriter(Stream stream, CancellationToken token = default)
        {
            _stream = stream;
            _token = token;
        }

#if NET462
        public Task WriteNAsync()
        {
            return _stream.WriteAsync(N, 0, 1, _token);
        }
#else
        public ValueTask WriteNAsync()
        {
            return _stream.WriteAsync(N, _token);
        }
#endif

#if NET462
        public Task WriteLineAsync(string line)
        {
            var bytes = Encoding.UTF8.GetBytes(line);
            return _stream.WriteAsync(bytes, 0, bytes.Length, _token);
        }
#else
        public ValueTask WriteLineAsync(string line)
        {
            var bytes = Encoding.UTF8.GetBytes(line);
            return _stream.WriteAsync(bytes, _token);
        }
#endif

#if NET462
        public Task WritePointDataAsync(PointData point)
#else
        public ValueTask WritePointDataAsync(PointData point)
#endif
        {
            if (point.Tags == null || point.Tags.Count == 0)
            {
                throw new Exception("you must add tag.");
            }

            var sb = new StringBuilder();
            sb.Append(point.Measurement);
            sb.Append(',');

            //Append Tags
            var i = 0;
            foreach (var tag in point.Tags)
            {
                if (string.IsNullOrEmpty(tag.Key))
                {
                    throw new Exception("tag key is null or empty.");
                }

                if (string.IsNullOrEmpty(tag.Value))
                {
                    throw new Exception($"tag=>{tag.Key} value is null or empty");
                }

                i++;
                if (i > 1)
                {
                    sb.Append(',');
                }

                EscapeKey(sb, tag.Key);
                sb.Append('=');
                EscapeKey(sb, tag.Value);
            }

            //Append Fields
            if (point.Fields == null || point.Fields.Count == 0)
            {
                throw new Exception("you must add filed.");
            }

            sb.Append(' ');
            i = 0;
            foreach (var field in point.Fields)
            {
                if (string.IsNullOrEmpty(field.Key))
                {
                    throw new Exception("field key is null or empty.");
                }

                i++;
                if (i > 1)
                {
                    sb.Append(',');
                }

                SetFields(sb, field.Key, field.Value);
            }

            //Append Time
            sb.Append(' ');

            if (point.Time.HasValue)
            {
                sb.Append(TimeUtils.DateTimeOffsetToUtcString(point.Time.Value, point.Precision));
            }
            else
            {
                sb.Append(TimeUtils.DateTimeOffsetToUtcString(DateTimeOffset.UtcNow, point.Precision));
            }
            return WriteLineAsync(sb.ToString());
        }

#if NET462
        public Task WriteAsync<T>(string measurement, T entity, TypeAccessor accssor = null, TypeCache ch = null) where T : class
#else
        public ValueTask WriteAsync<T>(string measurement, T entity, TypeAccessor accssor = null, TypeCache ch = null) where T : class
#endif

        {
            accssor ??= FastEmitFactory.GetTypeAccessor(typeof(T));
            ch ??= CacheHelper.GetCache(typeof(T));
            var line = CreateTypeData(measurement, entity, accssor, ch);
            return WriteLineAsync(line);
        }
    }
}
